'use strict'

const path = require('path')
const URL = require('url')

const _ = require('lodash')
const fetch = require('node-fetch')
const globby = require('globby')
const gulp = require('gulp')
const pump = require('pump')
const sri = require('sri-toolbox')
const toIco = require('to-ico')
const yamljs = require('js-yaml')

const fs = require('fs-extra')

const babel = require('gulp-babel')
const cssnano = require('gulp-cssnano')
const htmlmin = require('gulp-htmlmin')
const imagemin = require('gulp-imagemin')
const jsonmin = require('gulp-jsonmin')
const purify = require('gulp-purifycss')
const responsive = require('gulp-responsive')
const sequence = require('gulp-sequence')
const uglify = require('gulp-uglify')

const base = './'
const opts = { base }

const cb = e => e && console.log(e.message)
const readSource = file => fs.readFile(file, 'utf8')

const negatedGlobs = [
  '!_site/vendor/**/*',
  '!node_modules/**/*',
  '!vendor/**/*'
]

const plugins = {
  'babel': {
    'comments': false,
    'presets': ['babili']
  },

  'cssnano': {
    'autoprefixer': {
      'add': true,
      'browsers': [
        'last 2 versions',
        'android 4',
        'ie >= 10',
        'ios 6',
        'safari >= 8'
      ]
    }
  },

  'htmlmin': (() => {
    const html = {
      'collapseBooleanAttributes': true,
      'collapseWhitespace': true,
      'decodeEntities': true,
      'includeAutoGeneratedTags': false,
      'removeAttributeQuotes': true,
      'removeComments': true,
      'removeEmptyAttributes': true,
      'removeOptionalTags': true,
      'removeRedundantAttributes': true,
      'removeScriptTypeAttributes': true,
      'removeStyleLinkTypeAttributes': true,
      'sortAttributes': true,
      'sortClassName': true,
      'useShortDoctype': true
    }

    const xml = _.defaults({
      'html5': false,
      'removeAttributeQuotes': false
    }, html)

    return { html, xml }
  })(),

  'imagemin': [
    imagemin.optipng({
      'optimizationLevel': 7
    }),
    imagemin.svgo({
      'floatPrecision': 1,
      'plugins': [
        { 'removeDimensions': true },
        { 'removeTitle': true }
      ]
    })
  ],

  'purify': {
    'rejected': true,
    'whitelist': ['*carbon*']
  },

  'responsive': {
    'errorOnEnlargement': false,
    'errorOnUnusedImage': false,
    'silent': true,
    'stats': false,
    'withoutEnlargement': false
  },

  'uglify': {
    'compress': {
      'collapse_vars': true,
      'negate_iife': false,
      'pure_getters': true,
      'unsafe': true,
      'warnings': false
    }
  }
}

/*----------------------------------------------------------------------------*/

/**
 * Cleanup whitespace of file at `filePath`.
 *
 * @private
 * @param {string} filePath The path of the file to clean.
 * @returns {Promise} Returns the cleanup promise.
 */
function cleanFile(filePath) {
  return readSource(filePath)
    .then(source => fs.writeFile(filePath, cleanSource(source)))
}

/**
 * Cleanup whitespace of `source`.
 *
 * @private
 * @param {string} source The source to clean.
 * @returns {string} Returns the cleaned source.
 */
function cleanSource(source) {
  return source
    // Trim whitespace.
    .trim()
    // Consolidate multiple newlines.
    .replace(/^(?:\s*\n){2,}/gm, '\n')
    // Consolidate spaces.
    .replace(/ {2,}/g, ' ')
    // Repair indentation.
    .replace(/^ (?=[-\w]+:)/gm, '  ') +
    // Add trailing newline.
    '\n'
}

/**
 * A thin wrapper around `gulp.src` to enforce task-wide negated globs.
 *
 * @private
 * @param {Array|string} glob The glob to read.
 * @returns {Stream} Returns the Vinyl stream of globbed files.
 */
function gulpSrc(glob, opts) {
  glob = _.castArray(glob)
  glob.push(...negatedGlobs)
  return gulp.src(glob, opts)
}

/**
 * Converts a `yaml` string into an object.
 *
 * @private
 * @param {string} yaml The yaml to convert.
 * @returns {Object} Returns the parsed yaml object.
 */
function parseYAML(yaml) {
  // Replace aliases with anchor values to enable parsing.
  yaml.replace(/^ *&(\S+) +(\S+)/gm, (match, anchor, value) => {
    const reAlias = RegExp(`\\*${ _.escapeRegExp(anchor) }\\b`, 'g')
    yaml = yaml.replace(reAlias, () => value)
  })
  return yamljs.load(yaml)
}

/**
 * Converts `string` to a regexp.
 *
 * @private
 * @param {string} string The string to convert.
 * @returns {RegExp} Returns the converted regexp.
 */
function toRegExp(string) {
  return RegExp(`(?:^|\\b)${ _.escapeRegExp(string) }(?:\\b|$)`, 'gm')
}

/*----------------------------------------------------------------------------*/

gulp.task('build-config', () => {
  const update = (config, oldVer, newVer) => config
    // Update `release` anchor value.
    .replace(/(&release +)\S+/, (match, prelude) => prelude + newVer)
    // Update `release` alias build href.
    .replace(/(\*release:[\s\S]+?\bhref: *)(\S+)/, (match, prelude, href) =>
      prelude + href.replace(toRegExp(oldVer), newVer)
    )

  return readSource('_config.yml').then(config => {
    const args = process.argv.slice(3)
    const oldVer = /&release +([\d.]+)/.exec(config)[1]
    const newVer = args[args.findIndex(arg => arg == '--release') + 1] || oldVer

    config = update(config, oldVer, newVer)

    let entries = []
    const parsed = parseYAML(config)
    const push = ({ href, integrity }) => entries.push({ href, integrity })

    _.forOwn(parsed.builds, push)
    _.forOwn(parsed.vendor, items => items.forEach(push))

    return Promise.all(entries.map(({ href }) => fetch(href)))
      .then(respes => Promise.all(respes.map(resp => resp.text())))
      .then(bodies => fs.writeFile('_config.yml', bodies.reduce((config, body, index) =>
        config.replace(entries[index].integrity, sri.generate({ 'algorithms': ['sha384'] }, body))
      , config)))
  })
})

/*----------------------------------------------------------------------------*/

gulp.task('build-sw', () => {
  const escape = from => _.escapeRegExp(from)
    // Replace escaped asterisks with greedy dot capture groups.
    .replace(/\\\*/g, '(.*)')
    // Make trailing slashes optional.
    .replace(/\/$/, '(?:/.|/?$)')
    // Escape forward slashes.
    .replace(/\//g, '\\/')

  return Promise.all(['_site/_redirects', '_site/sw.js'].map(readSource))
    .then(({ 0:redirects, 1:sw }) => fs.writeFile('_site/sw.js', sw.replace('/*insert_redirect*/', () => {
      const rules = []
      redirects
        // Remove comments.
        .replace(/#.*/g, '')
        // Extract redirect rules.
        .replace(/^[\t ]*(\S+)[\t ]+(\S+)(?:[\t ]+(\S+))?/gm, (match, from, to, status) =>
          rules.push(`[/^${ escape(from) }/,'${ to }',${ status }]`)
        )
      return rules.join(', ')
    })))
})

/*----------------------------------------------------------------------------*/

gulp.task('build-vendor', () =>
  readSource('_config.yml').then(config => {
    const hrefs = []
    const parsed = parseYAML(config)
    const push = value => hrefs.push(value.href || value)

    _.forOwn(parsed.builds, push)
    _.forOwn(parsed['font-face'], styles => _.forOwn(styles, hrefs => hrefs.forEach(push)))
    _.forOwn(parsed.vendor, items => items.forEach(push))
    _.remove(hrefs, href => href.endsWith('/'))

    return Promise.all(hrefs.map(href => fetch(href)))
      .then(respes => Promise.all(respes.map(resp => resp.buffer())))
      .then(buffers => Promise.all(buffers.map((buffer, index) => {
        const url = URL.parse(hrefs[index])
        const withQuery = path.join('vendor', url.hostname + url.path)
        const withoutQuery = path.join('vendor', url.hostname + url.pathname)
        const newHref = '/' + withQuery.split(path.sep).join('/')

        config = config.replace(toRegExp(url.href), () => newHref)
        return fs.outputFile(withoutQuery, buffer)
      })))
      .then(() => fs.writeFile('_config.yml', config))
  })
)

/*----------------------------------------------------------------------------*/

gulp.task('build-appcache', () => cleanFile('_site/manifest.appcache'))
gulp.task('build-css', ['minify-css'])
gulp.task('build-headers', () => cleanFile('_site/_headers'))
gulp.task('build-html', ['minify-html'])
gulp.task('build-images', sequence('build-app-icons', 'build-favicon', 'minify-images'))
gulp.task('build-js', sequence('build-sw'))
gulp.task('build-metadata', ['build-appcache', 'minify-json', 'minify-xml'])
gulp.task('build-redirects', () => cleanFile('_site/_redirects'))

gulp.task('build-app-icons', () =>
  pump([
    gulpSrc(['**/*.{png,svg}', '!_site/**/*'], opts),
    responsive(require('./icons'), plugins.responsive),
    gulp.dest('_site/icons/')
  ], cb)
)

gulp.task('build-favicon', () =>
  globby('_site/icons/favicon-*.png')
    .then(files => Promise.all(files.map(file => fs.readFile(file))))
    .then(toIco)
    .then(buffer => fs.writeFile('_site/favicon.ico', buffer))
)

/*----------------------------------------------------------------------------*/

gulp.task('minify-css', () =>
  pump([
    gulpSrc('_site/**/*.css', opts),
    purify(['_site/**/*.html', '_site/assets/**/*.js'], plugins.purify),
    cssnano(plugins.cssnano),
    gulp.dest(base)
  ])
)

gulp.task('minify-html', () =>
  pump([
    gulpSrc('_site/**/*.html', opts),
    htmlmin(plugins.htmlmin.html),
    gulp.dest(base)
  ], cb)
)

gulp.task('minify-images', () =>
  pump([
    gulpSrc('_site/**/*.{png,svg}', opts),
    imagemin(plugins.imagemin),
    gulp.dest(base)
  ], cb)
)

gulp.task('minify-js', () =>
  pump([
    gulpSrc(['_site/**/*.js', '!_site/sw.js'], opts),
    uglify(plugins.uglify),
    gulp.dest(base)
  ], cb)
)

gulp.task('minify-json', () =>
  pump([
    gulpSrc('_site/**/*.json', opts),
    jsonmin(),
    gulp.dest(base)
  ], cb)
)

gulp.task('minify-sw', () =>
  pump([
    gulpSrc('_site/sw.js', opts),
    babel(plugins.babel),
    gulp.dest(base)
  ], cb)
)

gulp.task('minify-xml', () =>
  pump([
    gulpSrc('_site/**/*.xml', opts),
    htmlmin(plugins.htmlmin.xml),
    gulp.dest(base)
  ], cb)
)

/*----------------------------------------------------------------------------*/

gulp.task('build', sequence(
  ['build-headers', 'build-metadata', 'build-redirects'],
  ['build-css', 'build-html', 'build-images', 'build-js']
))
